/*-----------------------------------------------------------------------
 * MediaFrame is an Open Source streaming media platform in Java 
 * which provides a fast, easy to implement and extremely small applet 
 * that enables to view your audio/video content without having 
 * to rely on external player applications or bulky plug-ins.
 *
 *-----------------------------------------------------------------------
 *
 * We changed a lot of code and added a lot of functionality.
 * This includes, but not limited to, the following changes:
 * 1. The project was renamed to MediaFrame;
 * 2. The connection speed detection procedure was added;
 * 3. The JavaScript API functions were added;
 * 4. The pre and post image support functionality was added;
 * 5. The ability to save movie into the local disk was added;
 * 6. The inner buffer for a movie file was added;
 * 7. The click-through functionality was added;    
 * 8. The .zip files support was added;    
 * 9. The realtime feedback agent functionality was added.    
 * For the full list of the current functionality please visit 
 * the following web page: http://mediaframe.org/
 *    
 * 06 Jul 2002 - 19 Dec 2004 Konstantin Belous, Oleg Lebedev
 *
 *-----------------------------------------------------------------------
 * 29/01/00		Initial version. mdm@techie.com
/*-----------------------------------------------------------------------
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

package mediaframe.mpeg1.audio.player;

import mediaframe.mpeg1.audio.decoder.*;
import javax.sound.sampled.*;		// <-- not standard (Ron)
import javax.sound.sampled.Control.*;

import java.io.*;

/**
 * The <code>JavaSoundAudioDevice</code> class implements an audio
 * device by using the JavaSound API. 
 * 
 * @since 0.0.8
 * @author Mat McGowan
 */
public class JavaSoundAudioDevice implements AudioDevice
{
	/** The source audio line that plays the audio stream. */
	private SourceDataLine	source = null;
	/** The Audio Format for the MPEG audio stream. */
	private AudioFormat		fmt = null;
	/** The temporary buffer for storing the audio data. */
	private byte[]			byteBuf = new byte[1024];

	/** <tt>True</tt> if the audio player is playing the sound, <tt>false</tt> if the playback is paused. */
	private boolean			playing = false;

	/** The sample rate (speed) of the current audio playback (0 - the playback is paused, > 0 otherwise). */  
	private float			oldSampleRate = 0;

	/** Constanta, equals to four seconds in milliseconds. */
	private static final int bufferMs = 4000;

	/** <tt>True</tt> if this audio device is opened. */
	private	boolean			open = false;
	/** <tt>True</tt> if this audio device is stoped. */
	private boolean			stopped = false;

	/** The decoder that provides audio data to this audio device. */
	private Decoder			decoder = null;
	
	/** The master gain volume value of the Java Sound audio device. */
	private float 			masterGain = -1f;

	/**
	 * Opens this audio device. 
	 * 
	 * @param decoder	the decoder that will provide audio data
	 *					to this audio device. 
	 */
	public synchronized void open(Decoder decoder)
	{
		if (! open)
		{
			this.decoder = decoder;
			open = true;
		}
	}
	
	/**
	 * Returns <tt>false</tt> because the <code>JavaSoundAudioDevice</code> audio device is not a   
	 * crappy eight-bit eight khz ulaw decoder (browser).
	 */
	public boolean isEightBitEightKhzMuLaw() {
		return false;
	}

/*
	protected void setAudioFormat(AudioFormat _fmt)
	{
		fmt = _fmt;
	}
*/	
	/**
	 * Gets the Audio Format for the MPEG audio stream.
	 */
	protected AudioFormat getAudioFormat()
	{
		if (fmt==null) {

			fmt = new AudioFormat(decoder.getOutputFrequency(),
								  16,
								  decoder.getOutputChannels(),
								  true,
								  false);

		}
		System.out.println("format: " + fmt.toString());

		return fmt;
	}

	/**
	 * Obtains and opens the source audio line using the Audio Format of the MPEG audio stream.
	 * @throws JavaLayerException if the source data line could not be obtained and opened.
	 */	
	protected void createSource()
		throws JavaLayerException
	{
		Throwable t = null;
		try {
			AudioFormat afmt = getAudioFormat();    // jl data audio format
			Line.Info info = new DataLine.Info(SourceDataLine.class, fmt, bufferMs);  // jl Info
			source = (SourceDataLine) AudioSystem.getLine(info);
			source.open(afmt, millisecondsToBytes(afmt, bufferMs));
			synchronized(source) {
				source.start();
				pause();
			}
		}
		catch (RuntimeException ex) {
			t = ex;
		}
		catch (LinkageError ex) {
			t = ex;	
		}
		catch (LineUnavailableException ex) {
			t = ex;	
		}
		catch (Throwable ex) {
			t = ex;	
		}
		
		if(t != null)
			System.out.println("JavaSoundDevice.createSource(): " + t.toString());

		if (source == null)
			throw new JavaLayerException("Cannot obtain source audio line", t);

	}
	
	/**
	 * Calculates the number of bytes required for storing the audio samples 
	 * for the specified time and the audio format.
	 * @param _fmt the format of the audio stream.
	 * @param time the time in milliseconds that should be buffered.
	 * @return the number of bytes required for storing the audio samples.
	 */
	protected int millisecondsToBytes(AudioFormat _fmt, int time)
	{
		return (int)(time * (_fmt.getSampleRate() *
							 _fmt.getChannels() *
							 _fmt.getSampleSizeInBits()) / 8000.0);
	}
	
	/**
	 * Closes this audio device. If the device is currently playing 
	 * audio, playback is stopped immediately without flushing
	 * any buffered audio data. 
	 */
	public void close()
	{
//		if (source != null && source.isActive())
//			source.stop();
		if (source != null && source.isOpen())
			source.close();
		open = false;
		decoder = null;
	}

//	private static int count = 0;
	
	/**
	 * Writes audio data to this audio device. Audio data is
	 * assumed to be in the output format of the decoder. This
	 * method may return before the data has actually been sounded
	 * by the device if the device buffers audio samples.
	 * <p/> 
	 * If the audio device is not open, this method does nothing. 
	 * 
	 * @param samples	the samples to write to the audio device.
	 * @param offs		the offset into the array of the first sample to write.
	 * @param len		the number of samples from the array to write. 
	 * @throws JavaLayerException if the audio data could not be
	 *			written to the audio device. 
	 */
	public void write(short[] samples, int offs, int len)
		throws JavaLayerException
	{
		if(!open || stopped)
			return;

		if (source == null)
			createSource();

		byte[] b = toByteArray(samples, offs, len);

		// long now = System.currentTimeMillis();
		// int avail = source.available();
		// System.out.println("         ***********          dumpbuf: " + avail + " len: " + len * 2 + " fr: " + getFillRatio());

//		count = count + 1;

		while(source.available() <= (len * 2)) {
			if(!open || stopped)
				return;
			try {
				Thread.sleep(100);
			} catch (InterruptedException IE) {
			    throw new JavaLayerException (IE.toString());			}
		}
		source.write(b, 0, len * 2);
	}
	
	/**
	 * Gets the buffer that able to store up to <code>length</code> bytes.
	 * @param length min size of the temporary buffer.
	 * @return the buffer for storing the bytes.
	 */
	protected byte[] getByteArray(int length)
	{
		if (byteBuf.length < length)
		{
			byteBuf = new byte[length+1024];	
		}
		return byteBuf;
	}
	
	/**
	 * Returns the byte array with up to <code>len</code> audio samples (two bytes each) 
	 * from the <code>samples<code> array starting at offset <code>offset</code>. 
	 * @param samples the audio data.
	 * @param offs the start offset in the data.
	 * @param len the number of samples to copy. 
	 * @return the byte array with the audio data.
	 */
	protected byte[] toByteArray(short[] samples, int offs, int len)
	{
		byte[] b = getByteArray(len*2);
		int idx = 0;
		short s;
		while (len-- > 0)
		{
			s = samples[offs++];
			b[idx++] = (byte)s;
			b[idx++] = (byte)(s>>>8);
		}

		return b;
	}
	
	/**
	 * Waits for any buffered audio samples to be played by the
	 * audio device. This method should only be called prior 
	 * to closing the device. 
	 */
	public void flush()
	{
		if (source != null && open) {
			source.drain();	
		}
	}

	/**
	 * Start player playing.
	 */
	public void play() {
		playing = true;

		if (source != null) {
			if(oldSampleRate != 0) {
				FloatControl cSampleRate = (FloatControl)source.getControl(javax.sound.sampled.FloatControl.Type.SAMPLE_RATE);
			
				if (cSampleRate != null) {					cSampleRate.setValue(oldSampleRate);
				}

				oldSampleRate = 0;
				return;
			}
			source.start();
			
		} else 
			System.out.println("JavaSoundDevice.play(): cannot find data line!");
		
	}

	/**
	 * Sets the volume (in the range 0..1) of the audio playback. 
	 * @param volume the volume to set.
	 */
	public void setVolume(float volume) {
		if (source != null) {
			FloatControl cMasterGain = (FloatControl)source.getControl(javax.sound.sampled.FloatControl.Type.MASTER_GAIN);
			masterGain = (float) (20.0 * Math.log(volume)/Math.log(10));
			boolean mute = false;
			BooleanControl cMute = (BooleanControl)source.getControl(javax.sound.sampled.BooleanControl.Type.MUTE);
			if(cMute != null) {
				mute = cMute.getValue();
			}
			if (! mute && cMasterGain != null)
				cMasterGain.setValue(masterGain);
		}
	}


	/**
	 * Sets the mute state of the audio playback.
	 * @param mute the mute state (<tt>true</tt> if the mute on and the player doesn't play the audio). 
	 */
	public void setMute(boolean mute) {
		if (source != null) {
			if(! mute && masterGain != -1f) {
				FloatControl cMasterGain = (FloatControl)source.getControl(
					javax.sound.sampled.FloatControl.Type.MASTER_GAIN
				);
				if (cMasterGain != null)
					cMasterGain.setValue(masterGain);
			}
			BooleanControl cMute = (BooleanControl)source.getControl(javax.sound.sampled.BooleanControl.Type.MUTE);
			if(cMute != null) {
				cMute.setValue(mute);
			}
		}
	}

	/**
	 * Stop player playing.
	 */
	public void stop() {
		if (source != null) {			stopped = true;
			source.stop();
			source.flush();

			if(oldSampleRate != 0) {

				FloatControl cSampleRate = (FloatControl)source.getControl(javax.sound.sampled.FloatControl.Type.SAMPLE_RATE);
			
				if (cSampleRate != null) {					cSampleRate.setValue(oldSampleRate);
				}
				
				oldSampleRate = 0;
			}
		}
		else 
			System.out.println("JavaSoundDevice.stop(): cannot find data line!");
	}


	/**
	 * Stop player playing.
	 */
	public void pause() {
	
		playing = false;
		
		if (source != null) {
			FloatControl cSampleRate = (FloatControl)source.getControl(javax.sound.sampled.FloatControl.Type.SAMPLE_RATE);
			
			if (cSampleRate != null) {				oldSampleRate = cSampleRate.getValue();
				cSampleRate.setValue(0);
			}
		}
	}

	/**
	 * Is called if the end of file has been reached.
	 */
	public void endOfFile() {}
}